Cocos Creator 3D 渲染管线超强解读!
作者对于 3.0 渲染管线的理解非常深入。这篇文章可以帮助开发者快速熟悉 3.0 的渲染管线,尽快熟悉下一代渲染架构。 ——Cocos Minggo
作者:Kunkka
大家好!我是一名来自腾讯的 Cocos 开发者,从 Cocos-iPhone,Cocos2d-x lua,Cocos Creator 到 Cocos Creator 3D,算下来我使用 Cocos 引擎有差不多10年了。此前比较少写博客,这是第一次在 Cocos 社区写技术分享,欢迎大家在评论区或社区原帖与我进行交流!
欢迎关注我的 Github
https://github.com/kunka
前言
Cocos Creator 3D
刚刚发布了 3.0 Preview 版,首次将 2D 和 3D 版本合并到了一起,经过多个版本迭代,渲染架构大幅升级与优化,非常值得深入学习和研究,以下是官网的性能与框架
介绍:
多渲染后端框架,已支持 WebGL 1.0 和 WebGL 2.0 面向未来的底层渲染 API 设计 基于 Command Buffer 提交渲染数据
目前渲染相关文档并不完善,本文将从源码入手分析 Cocos Creator 3D 多渲染后端框架 GFX
,引擎默认的前向渲染管线
实现,以及如何实现自定义渲染管线
。
目录
多渲染后端框架 GFX WebGL 渲染过程 GFX 中的对象 GFX 渲染过程 Cocos Creator 3D 渲染管线 渲染架构 UML 前向渲染管线 自定义渲染管线
多渲染后端框架 GFX
GFX 是针对渲染层做的高级抽象和封装,以达到编写一次渲染代码,适配不同渲染后端的目的。
目前源码中可见引擎已经适配了以下几种渲染后端:
WebGL WebGL2 OpenGL ES2 OpenGL ES3 Metal Vulkan
❝GFX 可以理解为实现 Cocos Creator 3D 引擎渲染的最基础接口,实现自定义渲染只能通过 GFX 提供的接口和规则来编写,以往在 Cocos 引擎中直接编写 GL 代码的方式已经成为过去。
❞
GFX 接口设计更贴近 Vulkan
等下一代渲染接口,为了说明 GFX 如何抽象渲染层,我们通过WebGL
渲染做一个对比,然后再用 GFX 接口来实现同样的功能。
WebGL 渲染过程
下面的示例代码展示了一次简单的 WebGL
渲染,目的是显示一张 2D 纹理:
function prepare(){
// texture
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(/**...*/);
gl.texImage2D(/** fill image data */);
// shader
var shader = someCreateShaderFunc("vert...", "frag...");
gl.useProgram(shader);
// vertex buffer
var vb = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vb);
gl.bufferData(/** fill vert data */;
// bind attributes
var attr = gl.getAttribLocation(shader, 'a_position');
gl.enableVertexAttribArray(attr);
gl.vertexAttribPointer(attr, /**...*/);
var attr = gl.getAttribLocation(shader, 'a_texCoord');
gl.enableVertexAttribArray(attr);
gl.vertexAttribPointer(attr, /**...*/);
// indices buffer
var ib = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib);
gl.bufferData(/** fill indices data */;
}
prepare();
render();
function render(){
// begin draw
//gl.bindFramebuffer(gl.FRAMEBUFFER, /**...*/);
gl.clearColor(/**...*/);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.viewport(/**...*/);
/** set states: depth test, stencil test, blend ... */
gl.useProgram(shader);
// set uniforms
gl.uniform(/**...*/);
// draw
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
// end draw
//gl.bindFramebuffer(gl.FRAMEBUFFER, null);
requestAnimationFrame(render);
}
这里可以把渲染过程简单分为2个阶段:数据准备
和渲染执行
,渲染执行
又可分为数据绑定
和绘制调用
。实际游戏中渲染执行
中会反复执行多个 DrawCall
,直到完成一帧中的所有绘制。
数据准备
:创建和提交数据到 GPU创建纹理 创建并编译 Shader,也就是 WebGLProgram 准备顶点数据,绑定 attributes layout 准备索引数据 渲染执行
:数据绑定:(Set Pass Call) 绘制调用:(Draw Call) 绑定纹理 设置 uniform 参数(Shader 中的固定变量) 调用绘制函数 准备画布,清除颜色/深度缓冲,设置 viewport 等 渲染结束
❝
数据准备
:可以理解为模型数据(顶点和纹理等)的上传;
渲染执行
:每一帧都会调用,刷新游戏画面;❞
数据绑定
:一般抽象为材质数据,切换数据绑定则相当于切换不同的材质。
GFX 中的对象
Device
:抽象GFX 渲染设备,提供与设备交互的渲染接口,具体实例化为 WebGLDevice
,VKDevice
等。
此外定义了15种渲染对象类型:
export enum ObjectType {
UNKNOWN,
BUFFER,
TEXTURE,
RENDER_PASS,
FRAMEBUFFER,
SAMPLER,
SHADER,
DESCRIPTOR_SET_LAYOUT,
PIPELINE_LAYOUT,
PIPELINE_STATE,
DESCRIPTOR_SET,
INPUT_ASSEMBLER,
COMMAND_BUFFER,
FENCE,
QUEUE,
WINDOW,
}
其中 Fence
(同步信号)和 Window
在 WebGL 实现中并没有用到。Texture
,Sampler
,Shader
,FrameBuffer
比较好理解,跟 GL 对象差异不大,其他9种对象类型分别是:
Buffer
:抽象 VB,IB,UB 等各种数据缓冲。InputAssembler
:集中管理 VB,IB,Attributes,IndirectBuffer 等各种输入。DescriptorSet
:集中管理 UB,Texture,Sampler 等。DescriptorSetLayout
:描述 DescriptorSet 绑定的布局。(相当于告诉 GPU 如何读取 DescriptorSet 数据)CommandBuffer
:将每一个渲染动作抽象为命令提交到队列,submit 时统一执行,支持异步渲染。(为 Vulkan 等下一代渲染接口准备,WebGL 并未实现)Queue
:CommandBuffer 队列?(WebgGL 实现只有一个空的 Queue,CommandBuffer 只用了一个默认的)RenderPass
:存放颜色缓冲区和深度缓冲区,也就是画布。PipelineLayout DescriptorSetLayout
和其他扩展信息,如 WebGL2 实现中有 WebGL2PipelineLayout 信息。PipelineState
:主要由以下状态动态组建PipelineLayout Shader RenderPass RasterizerState:CullMode,PolygonMode 等 DepthStencilState:DepthTest,StencilTest等 BlendTarget:Blend 设置 BlendState:BlendTarget集合 等 InputState:Attributes
DescriptorSet
参考 Vulkan
中的概念:
GFX 渲染过程
基于上述定义的基础概念,如果使用 GFX
实现同样的功能,代码如下:
function prepare() {
// vertex buffer
const vertexBuffers = device.createBuffer(/** buffer info */);
vertexBuffers.update(/** fill vert data */);
// indices buffer
const indicesBuffers = device.createBuffer(/** buffer info */);
indicesBuffers.update(/** fill indices data */);
// bind attributes
const attributes: Attribute[] = [
new Attribute('a_position', Format.RG32F),
new Attribute('a_texCoord', Format.RG32F),
];
const IAInfo = new InputAssemblerInfo(attributes, [vertexBuffers], indicesBuffers);
const assmebler = device.createInputAssembler(IAInfo);
// material, texture(sampler), shader, pass
const material = new Material();
material.initialize({ effectName: 'some shader name' });
sampler = device.createSampler(/**samplerInfo*/);
texture = device.createTexture(/** ...*/);
const pass = material.passes[0];
const binding = pass.getBinding('mainTexture');
pass.bindTexture(bingding, texture);
shader = ShaderPool.get(pass.getShaderVariant());
const descriptorSet = DSPool.get(PassPool.get(pass.handle, PassView.DESCRIPTOR_SET));
descriptorSet.bindSampler(binding, sampler);
descriptorSet.update();
}
prepare();
render();
function render() {
device.acquire();
const cmdBuff = device.cmdBuff;
const framebuffer = root.framebuffer;
const renderArea = new Rect(0, 0, device.width, device.height);
cmdBuff.begin();
// bind framebuffer, clear, set states ...
cmdBuff.beginRenderPass(framebuffer.renderPass, framebuffer, renderArea/** */);
// bind PipelineState
const pass = material.passes[0];
const pso = PipelineStateManager.getOrCreatePipelineState(device, pass, shader, framebuffer.renderPass, assmebler);
cmdBuff.bindPipelineState(pso);
cmdBuff.bindDescriptorSet(SetIndex.MATERIAL, pass.descriptorSet);
cmdBuff.bindInputAssembler(assmebler);
// draw
cmdBuff.draw(assmebler);
cmdBuff.endRenderPass();
cmdBuff.end();
device.queue.submit([cmdBuff]);
device.present();
requestAnimationFrame(render);
}
整理一下 GFX
渲染流程:
数据准备
:创建和提交数据到 GPU创建 Material,初始化 Effect 创建 Texture 和 Sampler,并绑定 Texture 到 Pass 创建 InputAssembler 创建 GFX Shader 根据 Pass,从对象池获取 DescriptorSet, 绑定并更新 提交渲染指令
:准备画布 beginRenderPass调用绘制函数 draw(WebGL 直接绘制) 获取 PSO 对象,绑定 PSO 绑定 DescriptorSet 绑定 IA 数据绑定:(Set Pass Call) 绘制调用:(Draw Call) 执行渲染队列
提交并执行 CommandBuffer
对比 WebGL
可以发现,渲染流程几乎一模一样,这有利于我们快速学习,但是细节上却有很大的区别,这也是号称面向未来的渲染 API 设计的原因,这里在 GFX 之上又封装了一些概念:
Effect
:Cocos Creator 3D 独有语法的 Shader 原始文件,类似 Unity 的 ShaderLab。Pass
:包含 BlendState,RasterizerState 等所有信息,全部按位存于 handle 里面,非常的高效。Material
:对应一个 Effect,可以有多个 Pass。Shader
(GFX Shader):结合 pass 指定编译宏组合动态创建,非常灵活。
显然 GFX
抽象的接口使用起来更加方便和灵活,OpenGL
状态机只提供最细粒度的状态设置接口,如果渲染状态切换,OpenGL 需要设置一大堆标志位,现在可以直接切换 Pipeline
,并且 Cocos Creator 3D 使用了非常多的对象池来优化性能。
Cocos Creator 3D 渲染管线
Cocos Creator 3D 渲染管线基于 GFX 接口,再次做了一层封装,方便应用层灵活使用,大致的渲染流程如下:
其中 Camera
数量可以有多个,Canvas
(内含正交 OrthCamera)也可以有多个。Flow
和 Stage
都可以自定义和自由组合,Stage
负责执行具体渲染指令。
Cocos Creator 3D 渲染架构 UML
渲染相关类定义非常多,而且关系错综复杂,很多相互引用,这里列举一下几个关键类的含义:
Root
:可以理解为渲染大总管,集中管理所有渲染相关的对象,包含 RenderPipeline,RenderWindow,RenderScene,Cameras,UIRenderPipeline
:渲染管线,定义一组 RenderFlow 队列RenderWindow
:渲染窗口,可以是屏幕缓冲也可以是离屏缓冲,可能有多个RenderView
:渲染视图,Camera 对象的渲染层表示RenderScene
:整个 Scene 场景对应的渲染层对象UI
:Scene 场景中所有 Canvas 对应的渲染对象,统一由 UI 管理,Cocos Creator 3D 单独为 UI 创建了一个 RenderScene 用于存放 UI 渲染模型 UIBatchedModel,所以 Root 中一共有2个 RenderSceneRenderFlow
:定义一组渲染 StageRenderStage
:渲染具体的实现,如 ForwardStage,UIStage
Cocos Creator 3D 前向渲染管线
前向渲染管线是 Cocos Creator 3D 提供的默认渲染管线,实现了3个类型的Flow
:
ShadowFlow
:渲染阴影。ForwardFlow
:对应一个 3D Camera,可能有多个。UIFlow
:对应一个 ui_Canvas,可能有多个。
另外橙色步骤比较关键,主要负责性能优化:
GenBatchedModel
:UI 动态合批。SceneCulling
:裁剪渲染对象。FillQueue
:根据裁剪后的对象,填充 Instanced 队列,Batched队列 ,不透明队列,透明队列。
Cocos Creator 3D 自定义染管线
Cocos Creator 3D 支持自定义渲染管线,我们尝试在 ForwPipeline
中新建一个后处理 Flow
,右键依次新建 Forward Pipeline Asset
,PostRenderFlow
和 PostRenderStage
。
点击刚才创建的 Pipeline
资源,打开 Inspector
,设置对应的 Flow
和 Stage
,将 PostRenderFlow
插入 ForwardFlow
和 UIFlow
之间。
然后重载 PostRenderFlow
的 activate
方法,实现 Flow
的初始化
public activate(pipeline: ForwardPipeline) {
super.activate(pipeline);
// create framebuffer
}
重载 PostRenderStage
的 render
方法,实现自定义渲染逻辑:
render(view: RenderView) {
const pipeline = this._pipeline as ForwardPipeline;
const cmdBuff = pipeline.commandBuffers[0];
const device = pipeline.device;
const renderPass = this.frameBuffer!.renderPass;
cmdBuff.begin();
cmdBuff.beginRenderPass();
// insert custom render code here
cmdBuff.endRenderPass();
cmdBuff.end();
bufs[0] = cmdBuff;
device.queue.submit(bufs);
}
最后打开 Project Setting
,切换至我们的 Pipeline
,然后就可以运行了!
结束
Cocos Creator 的 3D 功能正在努力完善,后续会推出更多高级和实用功能,我们将在第一时间体验。
十分感谢一直免费开源的 Cocos,提供给我们直面源码的机会,祝福 Cocos 十周年生日快乐!
下一个十年更精彩!
参考文档
Cocos Creator 3D 用户手册:
https://docs.cocos.com/creator3d/manual/zh/
Mozilla WebGL API:
https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API
learnopengl.com:
https://learnopengl.com/
Vulkan 资源绑定和状态管理:
https://zhuanlan.zhihu.com/p/172479225
非常感谢 Kunkka
带来的技术分享,欢迎各位开发者点击「阅读原文」
查看原贴,为作者点赞,与作者进行交流学习!